home *** CD-ROM | disk | FTP | other *** search
/ PC Advisor 2010 April / PCA177.iso / ESSENTIALS / Firefox Setup.exe / nonlocalized / components / storage-mozStorage.js < prev    next >
Encoding:
Text File  |  2009-07-15  |  54.7 KB  |  1,584 lines

  1. /* ***** BEGIN LICENSE BLOCK *****
  2.  * Version: MPL 1.1/GPL 2.0/LGPL 2.1
  3.  *
  4.  * The contents of this file are subject to the Mozilla Public License Version
  5.  * 1.1 (the "License"); you may not use this file except in compliance with
  6.  * the License. You may obtain a copy of the License at
  7.  * http://www.mozilla.org/MPL/
  8.  *
  9.  * Software distributed under the License is distributed on an "AS IS" basis,
  10.  * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
  11.  * for the specific language governing rights and limitations under the
  12.  * License.
  13.  *
  14.  * The Original Code is mozilla.org code.
  15.  *
  16.  * The Initial Developer of the Original Code is Mozilla Corporation.
  17.  * Portions created by the Initial Developer are Copyright (C) 2007
  18.  * the Initial Developer. All Rights Reserved.
  19.  *
  20.  * Contributor(s):
  21.  *  Paul O'Shannessy <poshannessy@mozilla.com> (primary author)
  22.  *  Mrinal Kant <mrinal.kant@gmail.com> (original sqlite related changes)
  23.  *  Justin Dolske <dolske@mozilla.com> (encryption/decryption functions are
  24.  *                                     a lift from Justin's storage-Legacy.js)
  25.  *
  26.  * Alternatively, the contents of this file may be used under the terms of
  27.  * either the GNU General Public License Version 2 or later (the "GPL"), or
  28.  * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
  29.  * in which case the provisions of the GPL or the LGPL are applicable instead
  30.  * of those above. If you wish to allow use of your version of this file only
  31.  * under the terms of either the GPL or the LGPL, and not to allow others to
  32.  * use your version of this file under the terms of the MPL, indicate your
  33.  * decision by deleting the provisions above and replace them with the notice
  34.  * and other provisions required by the GPL or the LGPL. If you do not delete
  35.  * the provisions above, a recipient may use your version of this file under
  36.  * the terms of any one of the MPL, the GPL or the LGPL.
  37.  *
  38.  * ***** END LICENSE BLOCK ***** */
  39.  
  40.  
  41. const Cc = Components.classes;
  42. const Ci = Components.interfaces;
  43.  
  44. const DB_VERSION = 3; // The database schema version
  45.  
  46. const ENCTYPE_BASE64 = 0;
  47. const ENCTYPE_SDR = 1;
  48.  
  49. Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
  50.  
  51. function LoginManagerStorage_mozStorage() { };
  52.  
  53. LoginManagerStorage_mozStorage.prototype = {
  54.  
  55.     classDescription  : "LoginManagerStorage_mozStorage",
  56.     contractID : "@mozilla.org/login-manager/storage/mozStorage;1",
  57.     classID : Components.ID("{8c2023b9-175c-477e-9761-44ae7b549756}"),
  58.     QueryInterface : XPCOMUtils.generateQI([Ci.nsILoginManagerStorage]),
  59.  
  60.     __logService : null, // Console logging service, used for debugging.
  61.     get _logService() {
  62.         if (!this.__logService)
  63.             this.__logService = Cc["@mozilla.org/consoleservice;1"].
  64.                                 getService(Ci.nsIConsoleService);
  65.         return this.__logService;
  66.     },
  67.  
  68.     __decoderRing : null,  // nsSecretDecoderRing service
  69.     get _decoderRing() {
  70.         if (!this.__decoderRing)
  71.             this.__decoderRing = Cc["@mozilla.org/security/sdr;1"].
  72.                                  getService(Ci.nsISecretDecoderRing);
  73.         return this.__decoderRing;
  74.     },
  75.  
  76.     __utfConverter : null, // UCS2 <--> UTF8 string conversion
  77.     get _utfConverter() {
  78.         if (!this.__utfConverter) {
  79.             this.__utfConverter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
  80.                                   createInstance(Ci.nsIScriptableUnicodeConverter);
  81.             this.__utfConverter.charset = "UTF-8";
  82.         }
  83.         return this.__utfConverter;
  84.     },
  85.  
  86.     _utfConverterReset : function() {
  87.         this.__utfConverter = null;
  88.     },
  89.  
  90.     __profileDir: null,  // nsIFile for the user's profile dir
  91.     get _profileDir() {
  92.         if (!this.__profileDir)
  93.             this.__profileDir = Cc["@mozilla.org/file/directory_service;1"].
  94.                                 getService(Ci.nsIProperties).
  95.                                 get("ProfD", Ci.nsIFile);
  96.         return this.__profileDir;
  97.     },
  98.  
  99.     __storageService: null, // Storage service for using mozStorage
  100.     get _storageService() {
  101.         if (!this.__storageService)
  102.             this.__storageService = Cc["@mozilla.org/storage/service;1"].
  103.                                     getService(Ci.mozIStorageService);
  104.         return this.__storageService;
  105.     },
  106.  
  107.     __uuidService: null,
  108.     get _uuidService() {
  109.         if (!this.__uuidService)
  110.             this.__uuidService = Cc["@mozilla.org/uuid-generator;1"].
  111.                                  getService(Ci.nsIUUIDGenerator);
  112.         return this.__uuidService;
  113.     },
  114.  
  115.     __observerService : null,
  116.     get _observerService() {
  117.         if (!this.__observerService)
  118.             this.__observerService = Cc["@mozilla.org/observer-service;1"].
  119.                                      getService(Ci.nsIObserverService);
  120.         return this.__observerService;
  121.     },
  122.  
  123.  
  124.     // The current database schema.
  125.     _dbSchema: {
  126.         tables: {
  127.             moz_logins:         "id                 INTEGER PRIMARY KEY," +
  128.                                 "hostname           TEXT NOT NULL,"       +
  129.                                 "httpRealm          TEXT,"                +
  130.                                 "formSubmitURL      TEXT,"                +
  131.                                 "usernameField      TEXT NOT NULL,"       +
  132.                                 "passwordField      TEXT NOT NULL,"       +
  133.                                 "encryptedUsername  TEXT NOT NULL,"       +
  134.                                 "encryptedPassword  TEXT NOT NULL,"       +
  135.                                 "guid               TEXT,"                +
  136.                                 "encType            INTEGER",
  137.             // Changes must be reflected in this._dbAreExpectedColumnsPresent
  138.             //                          and this._searchLogins
  139.             moz_disabledHosts:  "id                 INTEGER PRIMARY KEY," +
  140.                                 "hostname           TEXT UNIQUE ON CONFLICT REPLACE",
  141.         },
  142.         indices: {
  143.           moz_logins_hostname_index: {
  144.             table: "moz_logins",
  145.             columns: ["hostname"]
  146.           },
  147.           moz_logins_hostname_formSubmitURL_index: {
  148.             table: "moz_logins",
  149.             columns: ["hostname", "formSubmitURL"]
  150.           },
  151.           moz_logins_hostname_httpRealm_index: {
  152.               table: "moz_logins",
  153.               columns: ["hostname", "httpRealm"]
  154.           },
  155.           moz_logins_guid_index: {
  156.               table: "moz_logins",
  157.               columns: ["guid"]
  158.           },
  159.           moz_logins_encType_index: {
  160.               table: "moz_logins",
  161.               columns: ["encType"]
  162.           }
  163.         }
  164.     },
  165.     _dbConnection : null,  // The database connection
  166.     _dbStmts      : null,  // Database statements for memoization
  167.  
  168.     _prefBranch   : null,  // Preferences service
  169.     _signonsFile  : null,  // nsIFile for "signons.sqlite"
  170.     _importFile   : null,  // nsIFile for import from legacy
  171.     _debug        : false, // mirrors signon.debug
  172.     _base64checked : false,
  173.  
  174.  
  175.     /*
  176.      * log
  177.      *
  178.      * Internal function for logging debug messages to the Error Console.
  179.      */
  180.     log : function (message) {
  181.         if (!this._debug)
  182.             return;
  183.         dump("PwMgr mozStorage: " + message + "\n");
  184.         this._logService.logStringMessage("PwMgr mozStorage: " + message);
  185.     },
  186.  
  187.  
  188.     /*
  189.      * initWithFile
  190.      *
  191.      * Initialize the component, but override the default filename locations.
  192.      * This is primarily used to the unit tests and profile migration.
  193.      * aImportFile is legacy storage file, aDBFile is a sqlite/mozStorage file.
  194.      */
  195.     initWithFile : function(aImportFile, aDBFile) {
  196.         if (aImportFile)
  197.             this._importFile = aImportFile;
  198.         if (aDBFile)
  199.             this._signonsFile = aDBFile;
  200.  
  201.         this.init();
  202.     },
  203.  
  204.  
  205.     /*
  206.      * init
  207.      *
  208.      * Initialize this storage component; import from legacy files, if
  209.      * necessary. Most of the work is done in _deferredInit.
  210.      */
  211.     init : function () {
  212.         this._dbStmts = [];
  213.  
  214.         // Connect to the correct preferences branch.
  215.         this._prefBranch = Cc["@mozilla.org/preferences-service;1"].
  216.                            getService(Ci.nsIPrefService);
  217.         this._prefBranch = this._prefBranch.getBranch("signon.");
  218.         this._prefBranch.QueryInterface(Ci.nsIPrefBranch2);
  219.  
  220.         this._debug = this._prefBranch.getBoolPref("debug");
  221.  
  222.         // Check to see if the internal PKCS#11 token has been initialized.
  223.         // If not, set a blank password.
  224.         let tokenDB = Cc["@mozilla.org/security/pk11tokendb;1"].
  225.                       getService(Ci.nsIPK11TokenDB);
  226.  
  227.         let token = tokenDB.getInternalKeyToken();
  228.         if (token.needsUserInit) {
  229.             this.log("Initializing key3.db with default blank password.");
  230.             token.initPassword("");
  231.         }
  232.  
  233.         let isFirstRun;
  234.         try {
  235.             // If initWithFile is calling us, _signonsFile may already be set.
  236.             if (!this._signonsFile) {
  237.                 // Initialize signons.sqlite
  238.                 this._signonsFile = this._profileDir.clone();
  239.                 this._signonsFile.append("signons.sqlite");
  240.             }
  241.             this.log("Opening database at " + this._signonsFile.path);
  242.  
  243.             // Initialize the database (create, migrate as necessary)
  244.             isFirstRun = this._dbInit();
  245.  
  246.             // On first run we want to import the default legacy storage files.
  247.             // Otherwise if passed a file, import from that.
  248.             if (isFirstRun && !this._importFile)
  249.                 this._importLegacySignons();
  250.             else if (this._importFile)
  251.                 this._importLegacySignons(this._importFile);
  252.  
  253.             this._initialized = true;
  254.         } catch (e) {
  255.             this.log("Initialization failed: " + e);
  256.             // If the import fails on first run, we want to delete the db
  257.             if (isFirstRun && e == "Import failed")
  258.                 this._dbCleanup(false);
  259.             throw "Initialization failed";
  260.         }
  261.     },
  262.  
  263.  
  264.     /*
  265.      * addLogin
  266.      *
  267.      */
  268.     addLogin : function (login) {
  269.         this._addLogin(login, false);
  270.     },
  271.  
  272.  
  273.     /*
  274.      * _addLogin
  275.      *
  276.      * Private function wrapping core addLogin functionality.
  277.      */
  278.     _addLogin : function (login, isEncrypted) {
  279.         let userCanceled, encUsername, encPassword;
  280.  
  281.         // Throws if there are bogus values.
  282.         this._checkLoginValues(login);
  283.  
  284.         if (isEncrypted) {
  285.             [encUsername, encPassword] = [login.username, login.password];
  286.         } else {
  287.             // Get the encrypted value of the username and password.
  288.             [encUsername, encPassword, userCanceled] = this._encryptLogin(login);
  289.             if (userCanceled)
  290.                 throw "User canceled master password entry, login not added.";
  291.         }
  292.  
  293.         // Clone the login, so we don't modify the caller's object.
  294.         let loginClone = login.clone();
  295.  
  296.         // Initialize the nsILoginMetaInfo fields, unless the caller gave us values
  297.         loginClone.QueryInterface(Ci.nsILoginMetaInfo);
  298.         if (loginClone.guid) {
  299.             if (!this._isGuidUnique(loginClone.guid))
  300.                 throw "specified GUID already exists";
  301.         } else {
  302.             loginClone.guid = this._uuidService.generateUUID().toString();
  303.         }
  304.  
  305.         // Determine encryption type
  306.         let encType = ENCTYPE_SDR;
  307.         if (isEncrypted &&
  308.             (encUsername.charAt(0) == '~' || encPassword.charAt(0) == '~'))
  309.             encType = ENCTYPE_BASE64;
  310.  
  311.         let query =
  312.             "INSERT INTO moz_logins " +
  313.             "(hostname, httpRealm, formSubmitURL, usernameField, " +
  314.              "passwordField, encryptedUsername, encryptedPassword, " +
  315.              "guid, encType) " +
  316.             "VALUES (:hostname, :httpRealm, :formSubmitURL, :usernameField, " +
  317.                     ":passwordField, :encryptedUsername, :encryptedPassword, " +
  318.                     ":guid, :encType)";
  319.  
  320.         let params = {
  321.             hostname:          loginClone.hostname,
  322.             httpRealm:         loginClone.httpRealm,
  323.             formSubmitURL:     loginClone.formSubmitURL,
  324.             usernameField:     loginClone.usernameField,
  325.             passwordField:     loginClone.passwordField,
  326.             encryptedUsername: encUsername,
  327.             encryptedPassword: encPassword,
  328.             guid:              loginClone.guid,
  329.             encType:           encType
  330.         };
  331.  
  332.         let stmt;
  333.         try {
  334.             stmt = this._dbCreateStatement(query, params);
  335.             stmt.execute();
  336.         } catch (e) {
  337.             this.log("_addLogin failed: " + e.name + " : " + e.message);
  338.             throw "Couldn't write to database, login not added.";
  339.         } finally {
  340.             stmt.reset();
  341.         }
  342.  
  343.         // Send a notification that a login was added.
  344.         if (!isEncrypted)
  345.             this._sendNotification("addLogin", loginClone);
  346.     },
  347.  
  348.  
  349.     /*
  350.      * removeLogin
  351.      *
  352.      */
  353.     removeLogin : function (login) {
  354.         let [idToDelete, storedLogin] = this._getIdForLogin(login);
  355.         if (!idToDelete)
  356.             throw "No matching logins";
  357.  
  358.         // Execute the statement & remove from DB
  359.         let query  = "DELETE FROM moz_logins WHERE id = :id";
  360.         let params = { id: idToDelete };
  361.         let stmt;
  362.         try {
  363.             stmt = this._dbCreateStatement(query, params);
  364.             stmt.execute();
  365.         } catch (e) {
  366.             this.log("_removeLogin failed: " + e.name + " : " + e.message);
  367.             throw "Couldn't write to database, login not removed.";
  368.         } finally {
  369.             stmt.reset();
  370.         }
  371.  
  372.         this._sendNotification("removeLogin", storedLogin);
  373.     },
  374.  
  375.  
  376.     /*
  377.      * modifyLogin
  378.      *
  379.      */
  380.     modifyLogin : function (oldLogin, newLoginData) {
  381.         let [idToModify, oldStoredLogin] = this._getIdForLogin(oldLogin);
  382.         if (!idToModify)
  383.             throw "No matching logins";
  384.         oldStoredLogin.QueryInterface(Ci.nsILoginMetaInfo);
  385.  
  386.         let newLogin;
  387.         if (newLoginData instanceof Ci.nsILoginInfo) {
  388.             // Clone the existing login to get its nsILoginMetaInfo, then init it
  389.             // with the replacement nsILoginInfo data from the new login.
  390.             newLogin = oldStoredLogin.clone();
  391.             newLogin.init(newLoginData.hostname,
  392.                           newLoginData.formSubmitURL, newLoginData.httpRealm,
  393.                           newLoginData.username, newLoginData.password,
  394.                           newLoginData.usernameField, newLoginData.passwordField);
  395.             newLogin.QueryInterface(Ci.nsILoginMetaInfo);
  396.         } else if (newLoginData instanceof Ci.nsIPropertyBag) {
  397.             // Clone the existing login, along with all its properties.
  398.             newLogin = oldStoredLogin.clone();
  399.             newLogin.QueryInterface(Ci.nsILoginMetaInfo);
  400.  
  401.             let propEnum = newLoginData.enumerator;
  402.             while (propEnum.hasMoreElements()) {
  403.                 let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
  404.                 switch (prop.name) {
  405.                     // nsILoginInfo properties...
  406.                     case "hostname":
  407.                     case "httpRealm":
  408.                     case "formSubmitURL":
  409.                     case "username":
  410.                     case "password":
  411.                     case "usernameField":
  412.                     case "passwordField":
  413.                         newLogin[prop.name] = prop.value;
  414.                         break;
  415.  
  416.                     // nsILoginMetaInfo properties...
  417.                     case "guid":
  418.                         newLogin.guid = prop.value;
  419.                         if (!this._isGuidUnique(newLogin.guid))
  420.                             throw "specified GUID already exists";
  421.                         break;
  422.  
  423.                     // Fail if caller requests setting an unknown property.
  424.                     default:
  425.                         throw "Unexpected propertybag item: " + prop.name;
  426.                 }
  427.             }
  428.         } else {
  429.             throw "newLoginData needs an expected interface!";
  430.         }
  431.  
  432.         // Throws if there are bogus values.
  433.         this._checkLoginValues(newLogin);
  434.  
  435.         // Get the encrypted value of the username and password.
  436.         let [encUsername, encPassword, userCanceled] = this._encryptLogin(newLogin);
  437.         if (userCanceled)
  438.             throw "User canceled master password entry, login not modified.";
  439.  
  440.         let query =
  441.             "UPDATE moz_logins " +
  442.             "SET hostname = :hostname, " +
  443.                 "httpRealm = :httpRealm, " +
  444.                 "formSubmitURL = :formSubmitURL, " +
  445.                 "usernameField = :usernameField, " +
  446.                 "passwordField = :passwordField, " +
  447.                 "encryptedUsername = :encryptedUsername, " +
  448.                 "encryptedPassword = :encryptedPassword, " +
  449.                 "guid = :guid, " +
  450.                 "encType = :encType " +
  451.             "WHERE id = :id";
  452.  
  453.         let params = {
  454.             id:                idToModify,
  455.             hostname:          newLogin.hostname,
  456.             httpRealm:         newLogin.httpRealm,
  457.             formSubmitURL:     newLogin.formSubmitURL,
  458.             usernameField:     newLogin.usernameField,
  459.             passwordField:     newLogin.passwordField,
  460.             encryptedUsername: encUsername,
  461.             encryptedPassword: encPassword,
  462.             guid:              newLogin.guid,
  463.             encType:           ENCTYPE_SDR
  464.         };
  465.  
  466.         let stmt;
  467.         try {
  468.             stmt = this._dbCreateStatement(query, params);
  469.             stmt.execute();
  470.         } catch (e) {
  471.             this.log("modifyLogin failed: " + e.name + " : " + e.message);
  472.             throw "Couldn't write to database, login not modified.";
  473.         } finally {
  474.             stmt.reset();
  475.         }
  476.  
  477.         this._sendNotification("modifyLogin", [oldStoredLogin, newLogin]);
  478.     },
  479.  
  480.  
  481.     /*
  482.      * getAllLogins
  483.      *
  484.      * Returns an array of nsILoginInfo.
  485.      */
  486.     getAllLogins : function (count) {
  487.         let userCanceled;
  488.         let [logins, ids] = this._searchLogins({});
  489.  
  490.         // decrypt entries for caller.
  491.         [logins, userCanceled] = this._decryptLogins(logins);
  492.  
  493.         if (userCanceled)
  494.             throw "User canceled Master Password entry";
  495.  
  496.         this.log("_getAllLogins: returning " + logins.length + " logins.");
  497.         count.value = logins.length; // needed for XPCOM
  498.         return logins;
  499.     },
  500.  
  501.  
  502.     /*
  503.      * getAllEncryptedLogins
  504.      *
  505.      * Not implemented. This interface was added to extract logins from the
  506.      * legacy storage module without decrypting them. Now that logins are in
  507.      * mozStorage, if the encrypted data is really needed it can be easily
  508.      * obtained with SQL and the mozStorage APIs.
  509.      */
  510.     getAllEncryptedLogins : function (count) {
  511.         throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  512.     },
  513.  
  514.  
  515.     /*
  516.      * searchLogins
  517.      *
  518.      * Public wrapper around _searchLogins to convert the nsIPropertyBag to a
  519.      * JavaScript object and decrypt the results.
  520.      *
  521.      * Returns an array of decrypted nsILoginInfo.
  522.      */
  523.     searchLogins : function(count, matchData) {
  524.         let realMatchData = {};
  525.         // Convert nsIPropertyBag to normal JS object
  526.         let propEnum = matchData.enumerator;
  527.         while (propEnum.hasMoreElements()) {
  528.             let prop = propEnum.getNext().QueryInterface(Ci.nsIProperty);
  529.             realMatchData[prop.name] = prop.value;
  530.         }
  531.  
  532.         let [logins, ids] = this._searchLogins(realMatchData);
  533.  
  534.         let userCanceled;
  535.         // Decrypt entries found for the caller.
  536.         [logins, userCanceled] = this._decryptLogins(logins);
  537.  
  538.         if (userCanceled)
  539.         throw "User canceled Master Password entry";
  540.  
  541.         count.value = logins.length; // needed for XPCOM
  542.         return logins;
  543.     },
  544.  
  545.  
  546.     /*
  547.      * _searchLogins
  548.      *
  549.      * Private method to perform arbitrary searches on any field. Decryption is
  550.      * left to the caller.
  551.      *
  552.      * Returns [logins, ids] for logins that match the arguments, where logins
  553.      * is an array of encrypted nsLoginInfo and ids is an array of associated
  554.      * ids in the database.
  555.      */
  556.     _searchLogins : function (matchData) {
  557.         let conditions = [], params = {};
  558.  
  559.         for (field in matchData) {
  560.             let value = matchData[field];
  561.             switch (field) {
  562.                 // Historical compatibility requires this special case
  563.                 case "formSubmitURL":
  564.                     if (value != null) {
  565.                         conditions.push("formSubmitURL = :formSubmitURL OR formSubmitURL = ''");
  566.                         params["formSubmitURL"] = value;
  567.                         break;
  568.                     }
  569.                 // Normal cases.
  570.                 case "hostname":
  571.                 case "httpRealm":
  572.                 case "id":
  573.                 case "usernameField":
  574.                 case "passwordField":
  575.                 case "encryptedUsername":
  576.                 case "encryptedPassword":
  577.                 case "guid":
  578.                 case "encType":
  579.                     if (value == null) {
  580.                         conditions.push(field + " isnull");
  581.                     } else {
  582.                         conditions.push(field + " = :" + field);
  583.                         params[field] = value;
  584.                     }
  585.                     break;
  586.                 // Fail if caller requests an unknown property.
  587.                 default:
  588.                     throw "Unexpected field: " + field;
  589.             }
  590.         }
  591.  
  592.         // Build query
  593.         let query = "SELECT * FROM moz_logins";
  594.         if (conditions.length) {
  595.             conditions = conditions.map(function(c) "(" + c + ")");
  596.             query += " WHERE " + conditions.join(" AND ");
  597.         }
  598.  
  599.         let stmt;
  600.         let logins = [], ids = [];
  601.         try {
  602.             stmt = this._dbCreateStatement(query, params);
  603.             // We can't execute as usual here, since we're iterating over rows
  604.             while (stmt.step()) {
  605.                 // Create the new nsLoginInfo object, push to array
  606.                 let login = Cc["@mozilla.org/login-manager/loginInfo;1"].
  607.                             createInstance(Ci.nsILoginInfo);
  608.                 login.init(stmt.row.hostname, stmt.row.formSubmitURL,
  609.                            stmt.row.httpRealm, stmt.row.encryptedUsername,
  610.                            stmt.row.encryptedPassword, stmt.row.usernameField,
  611.                            stmt.row.passwordField);
  612.                 // set nsILoginMetaInfo values
  613.                 login.QueryInterface(Ci.nsILoginMetaInfo);
  614.                 login.guid = stmt.row.guid;
  615.                 logins.push(login);
  616.                 ids.push(stmt.row.id);
  617.             }
  618.         } catch (e) {
  619.             this.log("_searchLogins failed: " + e.name + " : " + e.message);
  620.         } finally {
  621.             stmt.reset();
  622.         }
  623.  
  624.         this.log("_searchLogins: returning " + logins.length + " logins");
  625.         return [logins, ids];
  626.     },
  627.  
  628.  
  629.     /*
  630.      * removeAllLogins
  631.      *
  632.      * Removes all logins from storage.
  633.      */
  634.     removeAllLogins : function () {
  635.         this.log("Removing all logins");
  636.         // Delete any old, unused files.
  637.         this._removeOldSignonsFiles();
  638.  
  639.         // Disabled hosts kept, as one presumably doesn't want to erase those.
  640.         let query = "DELETE FROM moz_logins";
  641.         let stmt;
  642.         try {
  643.             stmt = this._dbCreateStatement(query);
  644.             stmt.execute();
  645.         } catch (e) {
  646.             this.log("_removeAllLogins failed: " + e.name + " : " + e.message);
  647.             throw "Couldn't write to database";
  648.         } finally {
  649.             stmt.reset();
  650.         }
  651.  
  652.         this._sendNotification("removeAllLogins", null);
  653.     },
  654.  
  655.  
  656.     /*
  657.      * getAllDisabledHosts
  658.      *
  659.      */
  660.     getAllDisabledHosts : function (count) {
  661.         let disabledHosts = this._queryDisabledHosts(null);
  662.  
  663.         this.log("_getAllDisabledHosts: returning " + disabledHosts.length + " disabled hosts.");
  664.         count.value = disabledHosts.length; // needed for XPCOM
  665.         return disabledHosts;
  666.     },
  667.  
  668.  
  669.     /*
  670.      * getLoginSavingEnabled
  671.      *
  672.      */
  673.     getLoginSavingEnabled : function (hostname) {
  674.         this.log("Getting login saving is enabled for " + hostname);
  675.         return this._queryDisabledHosts(hostname).length == 0
  676.     },
  677.  
  678.  
  679.     /*
  680.      * setLoginSavingEnabled
  681.      *
  682.      */
  683.     setLoginSavingEnabled : function (hostname, enabled) {
  684.         // Throws if there are bogus values.
  685.         this._checkHostnameValue(hostname);
  686.  
  687.         this.log("Setting login saving enabled for " + hostname + " to " + enabled);
  688.         let query;
  689.         if (enabled)
  690.             query = "DELETE FROM moz_disabledHosts " +
  691.                     "WHERE hostname = :hostname";
  692.         else
  693.             query = "INSERT INTO moz_disabledHosts " +
  694.                     "(hostname) VALUES (:hostname)";
  695.         let params = { hostname: hostname };
  696.  
  697.         let stmt
  698.         try {
  699.             stmt = this._dbCreateStatement(query, params);
  700.             stmt.execute();
  701.         } catch (e) {
  702.             this.log("setLoginSavingEnabled failed: " + e.name + " : " + e.message);
  703.             throw "Couldn't write to database"
  704.         } finally {
  705.             stmt.reset();
  706.         }
  707.  
  708.         this._sendNotification(enabled ? "hostSavingEnabled" : "hostSavingDisabled", hostname);
  709.     },
  710.  
  711.  
  712.     /*
  713.      * findLogins
  714.      *
  715.      */
  716.     findLogins : function (count, hostname, formSubmitURL, httpRealm) {
  717.         let userCanceled;
  718.         let loginData = {
  719.             hostname: hostname,
  720.             formSubmitURL: formSubmitURL,
  721.             httpRealm: httpRealm
  722.         };
  723.         let matchData = { };
  724.         for each (field in ["hostname", "formSubmitURL", "httpRealm"])
  725.           if (loginData[field] != '')
  726.               matchData[field] = loginData[field];
  727.         let [logins, ids] = this._searchLogins(matchData);
  728.  
  729.         // Decrypt entries found for the caller.
  730.         [logins, userCanceled] = this._decryptLogins(logins);
  731.  
  732.         // We want to throw in this case, so that the Login Manager
  733.         // knows to stop processing forms on the page so the user isn't
  734.         // prompted multiple times.
  735.         if (userCanceled)
  736.             throw "User canceled Master Password entry";
  737.  
  738.         this.log("_findLogins: returning " + logins.length + " logins");
  739.         count.value = logins.length; // needed for XPCOM
  740.         return logins;
  741.     },
  742.  
  743.  
  744.     /*
  745.      * countLogins
  746.      *
  747.      */
  748.     countLogins : function (hostname, formSubmitURL, httpRealm) {
  749.         // Do checks for null and empty strings, adjust conditions and params
  750.         let [conditions, params] =
  751.             this._buildConditionsAndParams(hostname, formSubmitURL, httpRealm);
  752.  
  753.         let query = "SELECT COUNT(1) AS numLogins FROM moz_logins";
  754.         if (conditions.length) {
  755.             conditions = conditions.map(function(c) "(" + c + ")");
  756.             query += " WHERE " + conditions.join(" AND ");
  757.         }
  758.  
  759.         let stmt, numLogins;
  760.         try {
  761.             stmt = this._dbCreateStatement(query, params);
  762.             stmt.step();
  763.             numLogins = stmt.row.numLogins;
  764.         } catch (e) {
  765.             this.log("_countLogins failed: " + e.name + " : " + e.message);
  766.         } finally {
  767.             stmt.reset();
  768.         }
  769.  
  770.         this.log("_countLogins: counted logins: " + numLogins);
  771.         return numLogins;
  772.     },
  773.  
  774.  
  775.     /*
  776.      * _sendNotification
  777.      *
  778.      * Send a notification when stored data is changed.
  779.      */
  780.     _sendNotification : function (changeType, data) {
  781.         let dataObject = data;
  782.         // Can't pass a raw JS string or array though notifyObservers(). :-(
  783.         if (data instanceof Array) {
  784.             dataObject = Cc["@mozilla.org/array;1"].
  785.                          createInstance(Ci.nsIMutableArray);
  786.             for (let i = 0; i < data.length; i++)
  787.                 dataObject.appendElement(data[i], false);
  788.         } else if (typeof(data) == "string") {
  789.             dataObject = Cc["@mozilla.org/supports-string;1"].
  790.                          createInstance(Ci.nsISupportsString);
  791.             dataObject.data = data;
  792.         }
  793.         this._observerService.notifyObservers(dataObject, "passwordmgr-storage-changed", changeType);
  794.     },
  795.  
  796.  
  797.     /*
  798.      * _getIdForLogin
  799.      *
  800.      * Returns an array with two items: [id, login]. If the login was not
  801.      * found, both items will be null. The returned login contains the actual
  802.      * stored login (useful for looking at the actual nsILoginMetaInfo values).
  803.      */
  804.     _getIdForLogin : function (login) {
  805.         let matchData = { };
  806.         for each (field in ["hostname", "formSubmitURL", "httpRealm"])
  807.             if (login[field] != '')
  808.                 matchData[field] = login[field];
  809.         let [logins, ids] = this._searchLogins(matchData);
  810.  
  811.         let id = null;
  812.         let foundLogin = null;
  813.  
  814.         // The specified login isn't encrypted, so we need to ensure
  815.         // the logins we're comparing with are decrypted. We decrypt one entry
  816.         // at a time, lest _decryptLogins return fewer entries and screw up
  817.         // indices between the two.
  818.         for (let i = 0; i < logins.length; i++) {
  819.             let [[decryptedLogin], userCanceled] =
  820.                         this._decryptLogins([logins[i]]);
  821.  
  822.             if (userCanceled)
  823.                 throw "User canceled master password entry.";
  824.  
  825.             if (!decryptedLogin || !decryptedLogin.equals(login))
  826.                 continue;
  827.  
  828.             // We've found a match, set id and break
  829.             foundLogin = decryptedLogin;
  830.             id = ids[i];
  831.             break;
  832.         }
  833.  
  834.         return [id, foundLogin];
  835.     },
  836.  
  837.  
  838.     /*
  839.      * _queryDisabledHosts
  840.      *
  841.      * Returns an array of hostnames from the database according to the
  842.      * criteria given in the argument. If the argument hostname is null, the
  843.      * result array contains all hostnames
  844.      */
  845.     _queryDisabledHosts : function (hostname) {
  846.         let disabledHosts = [];
  847.  
  848.         let query = "SELECT hostname FROM moz_disabledHosts";
  849.         let params = {};
  850.         if (hostname) {
  851.             query += " WHERE hostname = :hostname";
  852.             params = { hostname: hostname };
  853.         }
  854.  
  855.         let stmt;
  856.         try {
  857.             stmt = this._dbCreateStatement(query, params);
  858.             while (stmt.step())
  859.                 disabledHosts.push(stmt.row.hostname);
  860.         } catch (e) {
  861.             this.log("_queryDisabledHosts failed: " + e.name + " : " + e.message);
  862.         } finally {
  863.             stmt.reset();
  864.         }
  865.  
  866.         return disabledHosts;
  867.     },
  868.  
  869.  
  870.     /*
  871.      * _buildConditionsAndParams
  872.      *
  873.      * Adjusts the WHERE conditions and parameters for statements prior to the
  874.      * statement being created. This fixes the cases where nulls are involved
  875.      * and the empty string is supposed to be a wildcard match
  876.      */
  877.     _buildConditionsAndParams : function (hostname, formSubmitURL, httpRealm) {
  878.         let conditions = [], params = {};
  879.  
  880.         if (hostname == null) {
  881.             conditions.push("hostname isnull");
  882.         } else if (hostname != '') {
  883.             conditions.push("hostname = :hostname");
  884.             params["hostname"] = hostname;
  885.         }
  886.  
  887.         if (formSubmitURL == null) {
  888.             conditions.push("formSubmitURL isnull");
  889.         } else if (formSubmitURL != '') {
  890.             conditions.push("formSubmitURL = :formSubmitURL OR formSubmitURL = ''");
  891.             params["formSubmitURL"] = formSubmitURL;
  892.         }
  893.  
  894.         if (httpRealm == null) {
  895.             conditions.push("httpRealm isnull");
  896.         } else if (httpRealm != '') {
  897.             conditions.push("httpRealm = :httpRealm");
  898.             params["httpRealm"] = httpRealm;
  899.         }
  900.  
  901.         return [conditions, params];
  902.     },
  903.  
  904.  
  905.     /*
  906.      * _checkLoginValues
  907.      *
  908.      * Due to the way the signons2.txt file is formatted, we need to make
  909.      * sure certain field values or characters do not cause the file to
  910.      * be parse incorrectly. Reject logins that we can't store correctly.
  911.      */
  912.     _checkLoginValues : function (aLogin) {
  913.         function badCharacterPresent(l, c) {
  914.             return ((l.formSubmitURL && l.formSubmitURL.indexOf(c) != -1) ||
  915.                     (l.httpRealm     && l.httpRealm.indexOf(c)     != -1) ||
  916.                                         l.hostname.indexOf(c)      != -1  ||
  917.                                         l.usernameField.indexOf(c) != -1  ||
  918.                                         l.passwordField.indexOf(c) != -1);
  919.         }
  920.  
  921.         // Nulls are invalid, as they don't round-trip well.
  922.         // Mostly not a formatting problem, although ".\0" can be quirky.
  923.         if (badCharacterPresent(aLogin, "\0"))
  924.             throw "login values can't contain nulls";
  925.  
  926.         // In theory these nulls should just be rolled up into the encrypted
  927.         // values, but nsISecretDecoderRing doesn't use nsStrings, so the
  928.         // nulls cause truncation. Check for them here just to avoid
  929.         // unexpected round-trip surprises.
  930.         if (aLogin.username.indexOf("\0") != -1 ||
  931.             aLogin.password.indexOf("\0") != -1)
  932.             throw "login values can't contain nulls";
  933.  
  934.         // Newlines are invalid for any field stored as plaintext.
  935.         if (badCharacterPresent(aLogin, "\r") ||
  936.             badCharacterPresent(aLogin, "\n"))
  937.             throw "login values can't contain newlines";
  938.  
  939.         // A line with just a "." can have special meaning.
  940.         if (aLogin.usernameField == "." ||
  941.             aLogin.formSubmitURL == ".")
  942.             throw "login values can't be periods";
  943.  
  944.         // A hostname with "\ \(" won't roundtrip.
  945.         // eg host="foo (", realm="bar" --> "foo ( (bar)"
  946.         // vs host="foo", realm=" (bar" --> "foo ( (bar)"
  947.         if (aLogin.hostname.indexOf(" (") != -1)
  948.             throw "bad parens in hostname";
  949.     },
  950.  
  951.  
  952.     /*
  953.      * _checkHostnameValue
  954.      *
  955.      * Legacy storage prohibited newlines and nulls in hostnames, so we'll keep
  956.      * that standard here. Throws on illegal format.
  957.      */
  958.     _checkHostnameValue : function (hostname) {
  959.         // File format prohibits certain values. Also, nulls
  960.         // won't round-trip with getAllDisabledHosts().
  961.         if (hostname == "." ||
  962.             hostname.indexOf("\r") != -1 ||
  963.             hostname.indexOf("\n") != -1 ||
  964.             hostname.indexOf("\0") != -1)
  965.             throw "Invalid hostname";
  966.     },
  967.  
  968.  
  969.     /*
  970.      * _isGuidUnique
  971.      *
  972.      * Checks to see if the specified GUID already exists.
  973.      */
  974.     _isGuidUnique : function (guid) {
  975.         let query = "SELECT COUNT(1) AS numLogins FROM moz_logins WHERE guid = :guid";
  976.         let params = { guid: guid };
  977.  
  978.         let stmt, numLogins;
  979.         try {
  980.             stmt = this._dbCreateStatement(query, params);
  981.             stmt.step();
  982.             numLogins = stmt.row.numLogins;
  983.         } catch (e) {
  984.             this.log("_isGuidUnique failed: " + e.name + " : " + e.message);
  985.         } finally {
  986.             stmt.reset();
  987.         }
  988.  
  989.         return (numLogins == 0);
  990.     },
  991.  
  992.  
  993.     /*
  994.      * _importLegacySignons
  995.      *
  996.      * Imports a file that uses Legacy storage. Will use importFile if provided
  997.      * else it will attempt to initialize the Legacy storage normally.
  998.      *
  999.      */
  1000.     _importLegacySignons : function (importFile) {
  1001.         this.log("Importing " + (importFile ? importFile.path : "legacy storage"));
  1002.  
  1003.         let legacy = Cc["@mozilla.org/login-manager/storage/legacy;1"].
  1004.                      createInstance(Ci.nsILoginManagerStorage);
  1005.  
  1006.         // Import all logins and disabled hosts
  1007.         try {
  1008.             if (importFile)
  1009.                 legacy.initWithFile(importFile, null);
  1010.             else
  1011.                 legacy.init();
  1012.  
  1013.             // Import logins and disabledHosts
  1014.             let logins = legacy.getAllEncryptedLogins({});
  1015.  
  1016.             // Wrap in a transaction for better performance.
  1017.             this._dbConnection.beginTransaction();
  1018.             for each (let login in logins)
  1019.                 this._addLogin(login, true);
  1020.             let disabledHosts = legacy.getAllDisabledHosts({});
  1021.             for each (let hostname in disabledHosts)
  1022.                 this.setLoginSavingEnabled(hostname, false);
  1023.             this._dbConnection.commitTransaction();
  1024.         } catch (e) {
  1025.             this.log("_importLegacySignons failed: " + e.name + " : " + e.message);
  1026.             throw "Import failed";
  1027.         }
  1028.     },
  1029.  
  1030.  
  1031.     /*
  1032.      * _removeOldSignonsFiles
  1033.      *
  1034.      * Deletes any storage files that we're not using any more.
  1035.      */
  1036.     _removeOldSignonsFiles : function () {
  1037.         // We've used a number of prefs over time due to compatibility issues.
  1038.         // We want to delete all files referenced in prefs, which are only for
  1039.         // importing and clearing logins from storage-Legacy.js.
  1040.         filenamePrefs = ["SignonFileName3", "SignonFileName2", "SignonFileName"];
  1041.         for each (let prefname in filenamePrefs) {
  1042.             let filename = this._prefBranch.getCharPref(prefname);
  1043.             let file = this._profileDir.clone();
  1044.             file.append(filename);
  1045.  
  1046.             if (file.exists()) {
  1047.                 this.log("Deleting old " + filename + " (" + prefname + ")");
  1048.                 try {
  1049.                     file.remove(false);
  1050.                 } catch (e) {
  1051.                     this.log("NOTICE: Couldn't delete " + filename + ": " + e);
  1052.                 }
  1053.             }
  1054.         }
  1055.     },
  1056.  
  1057.  
  1058.     /*
  1059.      * _encryptLogin
  1060.      *
  1061.      * Returns the encrypted username and password for the specified login,
  1062.      * and a boolean indicating if the user canceled the master password entry
  1063.      * (in which case no encrypted values are returned).
  1064.      */
  1065.     _encryptLogin : function (login) {
  1066.         let encUsername, encPassword, userCanceled;
  1067.         [encUsername, userCanceled] = this._encrypt(login.username);
  1068.         if (userCanceled)
  1069.             return [null, null, true];
  1070.  
  1071.         [encPassword, userCanceled] = this._encrypt(login.password);
  1072.         // Probably can't hit this case, but for completeness...
  1073.         if (userCanceled)
  1074.             return [null, null, true];
  1075.  
  1076.         if (!this._base64checked)
  1077.             this._reencryptBase64Logins();
  1078.  
  1079.         return [encUsername, encPassword, false];
  1080.     },
  1081.  
  1082.  
  1083.     /*
  1084.      * _decryptLogins
  1085.      *
  1086.      * Decrypts username and password fields in the provided array of
  1087.      * logins.
  1088.      *
  1089.      * The entries specified by the array will be decrypted, if possible.
  1090.      * An array of successfully decrypted logins will be returned. The return
  1091.      * value should be given to external callers (since still-encrypted
  1092.      * entries are useless), whereas internal callers generally don't want
  1093.      * to lose unencrypted entries (eg, because the user clicked Cancel
  1094.      * instead of entering their master password)
  1095.      */
  1096.     _decryptLogins : function (logins) {
  1097.         let result = [], userCanceled = false;
  1098.  
  1099.         for each (let login in logins) {
  1100.             let decryptedUsername, decryptedPassword;
  1101.  
  1102.             [decryptedUsername, userCanceled] = this._decrypt(login.username);
  1103.  
  1104.             if (userCanceled)
  1105.                 break;
  1106.  
  1107.             [decryptedPassword, userCanceled] = this._decrypt(login.password);
  1108.  
  1109.             // Probably can't hit this case, but for completeness...
  1110.             if (userCanceled)
  1111.                 break;
  1112.  
  1113.             // If decryption failed (corrupt entry?) skip it.
  1114.             // Note that we allow password-only logins, so username can be "".
  1115.             if (decryptedUsername == null || !decryptedPassword)
  1116.                 continue;
  1117.  
  1118.             login.username = decryptedUsername;
  1119.             login.password = decryptedPassword;
  1120.  
  1121.             result.push(login);
  1122.         }
  1123.  
  1124.         if (!this._base64checked && !userCanceled)
  1125.             this._reencryptBase64Logins();
  1126.  
  1127.         return [result, userCanceled];
  1128.     },
  1129.  
  1130.  
  1131.     /*
  1132.      * _reencryptBase64Logins
  1133.      *
  1134.      * Checks the signons DB for any logins using the old wallet-style base64
  1135.      * obscuring of the username/password, instead of proper encryption. We're
  1136.      * called once per session, after the user has successfully encrypted or
  1137.      * decrypted some login (this helps ensure the user doesn't get mysterious
  1138.      * prompts for a master password, when set).
  1139.      */
  1140.     _reencryptBase64Logins : function () {
  1141.         this._base64checked = true;
  1142.         // Ignore failures, will try again next session...
  1143.  
  1144.         try {
  1145.             let [logins, ids] = this._searchLogins({ encType: 0 });
  1146.  
  1147.             if (!logins.length)
  1148.                 return;
  1149.  
  1150.             let userCancelled;
  1151.             [logins, userCanceled] = this._decryptLogins(logins);
  1152.             if (userCanceled)
  1153.                 return;
  1154.  
  1155.             for each (let login in logins)
  1156.                 this.modifyLogin(login, login);
  1157.         } catch (e) {
  1158.             this.log("_reencryptBase64Logins caught error: " + e);
  1159.         }
  1160.     },
  1161.  
  1162.  
  1163.     /*
  1164.      * _encrypt
  1165.      *
  1166.      * Encrypts the specified string, using the SecretDecoderRing.
  1167.      *
  1168.      * Returns [cipherText, userCanceled] where:
  1169.      *  cipherText   -- the encrypted string, or null if it failed.
  1170.      *  userCanceled -- if the encryption failed, this is true if the
  1171.      *                  user selected Cancel when prompted to enter their
  1172.      *                  Master Password. The caller should bail out, and not
  1173.      *                  not request that more things be encrypted (which
  1174.      *                  results in prompting the user for a Master Password
  1175.      *                  over and over.)
  1176.      */
  1177.     _encrypt : function (plainText) {
  1178.         let cipherText = null, userCanceled = false;
  1179.  
  1180.         try {
  1181.             let plainOctet = this._utfConverter.ConvertFromUnicode(plainText);
  1182.             plainOctet += this._utfConverter.Finish();
  1183.             cipherText = this._decoderRing.encryptString(plainOctet);
  1184.         } catch (e) {
  1185.             this.log("Failed to encrypt string. (" + e.name + ")");
  1186.             // If the user clicks Cancel, we get NS_ERROR_FAILURE.
  1187.             // (unlike decrypting, which gets NS_ERROR_NOT_AVAILABLE).
  1188.             if (e.result == Components.results.NS_ERROR_FAILURE)
  1189.                 userCanceled = true;
  1190.         }
  1191.  
  1192.         return [cipherText, userCanceled];
  1193.     },
  1194.  
  1195.  
  1196.     /*
  1197.      * _decrypt
  1198.      *
  1199.      * Decrypts the specified string, using the SecretDecoderRing.
  1200.      *
  1201.      * Returns [plainText, userCanceled] where:
  1202.      *  plainText    -- the decrypted string, or null if it failed.
  1203.      *  userCanceled -- if the decryption failed, this is true if the
  1204.      *                  user selected Cancel when prompted to enter their
  1205.      *                  Master Password. The caller should bail out, and not
  1206.      *                  not request that more things be decrypted (which
  1207.      *                  results in prompting the user for a Master Password
  1208.      *                  over and over.)
  1209.      */
  1210.     _decrypt : function (cipherText) {
  1211.         let plainText = null, userCanceled = false;
  1212.  
  1213.         try {
  1214.             let plainOctet;
  1215.             if (cipherText.charAt(0) == '~') {
  1216.                 // The old Wallet file format obscured entries by
  1217.                 // base64-encoding them. These entries are signaled by a
  1218.                 // leading '~' character.
  1219.                 plainOctet = atob(cipherText.substring(1));
  1220.             } else {
  1221.                 plainOctet = this._decoderRing.decryptString(cipherText);
  1222.             }
  1223.             plainText = this._utfConverter.ConvertToUnicode(plainOctet);
  1224.         } catch (e) {
  1225.             this.log("Failed to decrypt string: " + cipherText +
  1226.                 " (" + e.name + ")");
  1227.  
  1228.             // In the unlikely event the converter threw, reset it.
  1229.             this._utfConverterReset();
  1230.  
  1231.             // If the user clicks Cancel, we get NS_ERROR_NOT_AVAILABLE.
  1232.             // If the cipherText is bad / wrong key, we get NS_ERROR_FAILURE
  1233.             // Wrong passwords are handled by the decoderRing reprompting;
  1234.             // we get no notification.
  1235.             if (e.result == Components.results.NS_ERROR_NOT_AVAILABLE)
  1236.                 userCanceled = true;
  1237.         }
  1238.  
  1239.         return [plainText, userCanceled];
  1240.     },
  1241.  
  1242.  
  1243.     //**************************************************************************//
  1244.     // Database Creation & Access
  1245.  
  1246.     /*
  1247.      * _dbCreateStatement
  1248.      *
  1249.      * Creates a statement, wraps it, and then does parameter replacement
  1250.      * Returns the wrapped statement for execution.  Will use memoization
  1251.      * so that statements can be reused.
  1252.      */
  1253.     _dbCreateStatement : function (query, params) {
  1254.         let wrappedStmt = this._dbStmts[query];
  1255.         // Memoize the statements
  1256.         if (!wrappedStmt) {
  1257.             this.log("Creating new statement for query: " + query);
  1258.             let stmt = this._dbConnection.createStatement(query);
  1259.  
  1260.             wrappedStmt = Cc["@mozilla.org/storage/statement-wrapper;1"].
  1261.                           createInstance(Ci.mozIStorageStatementWrapper);
  1262.             wrappedStmt.initialize(stmt);
  1263.             this._dbStmts[query] = wrappedStmt;
  1264.         }
  1265.         // Replace parameters, must be done 1 at a time
  1266.         if (params)
  1267.             for (let i in params)
  1268.                 wrappedStmt.params[i] = params[i];
  1269.         return wrappedStmt;
  1270.     },
  1271.  
  1272.  
  1273.     /*
  1274.      * _dbInit
  1275.      *
  1276.      * Attempts to initialize the database. This creates the file if it doesn't
  1277.      * exist, performs any migrations, etc. When database is first created, we
  1278.      * attempt to import legacy signons. Return if this is the first run.
  1279.      */
  1280.     _dbInit : function () {
  1281.         this.log("Initializing Database");
  1282.         let isFirstRun = false;
  1283.         try {
  1284.             this._dbConnection = this._storageService.openDatabase(this._signonsFile);
  1285.             // Get the version of the schema in the file. It will be 0 if the
  1286.             // database has not been created yet.
  1287.             let version = this._dbConnection.schemaVersion;
  1288.             if (version == 0) {
  1289.                 this._dbCreate();
  1290.                 isFirstRun = true;
  1291.             } else if (version != DB_VERSION) {
  1292.                 this._dbMigrate(version);
  1293.             }
  1294.         } catch (e if e.result == Components.results.NS_ERROR_FILE_CORRUPTED) {
  1295.             // Database is corrupted, so we backup the database, then throw
  1296.             // causing initialization to fail and a new db to be created next use
  1297.             this._dbCleanup(true);
  1298.             throw e;
  1299.         }
  1300.         return isFirstRun;
  1301.     },
  1302.  
  1303.  
  1304.     _dbCreate: function () {
  1305.         this.log("Creating Database");
  1306.         this._dbCreateSchema();
  1307.         this._dbConnection.schemaVersion = DB_VERSION;
  1308.     },
  1309.  
  1310.  
  1311.     _dbCreateSchema : function () {
  1312.         this._dbCreateTables();
  1313.         this._dbCreateIndices();
  1314.     },
  1315.  
  1316.  
  1317.     _dbCreateTables : function () {
  1318.         this.log("Creating Tables");
  1319.         for (let name in this._dbSchema.tables)
  1320.             this._dbConnection.createTable(name, this._dbSchema.tables[name]);
  1321.     },
  1322.  
  1323.  
  1324.     _dbCreateIndices : function () {
  1325.         this.log("Creating Indices");
  1326.         for (let name in this._dbSchema.indices) {
  1327.             let index = this._dbSchema.indices[name];
  1328.             let statement = "CREATE INDEX IF NOT EXISTS " + name + " ON " + index.table +
  1329.                             "(" + index.columns.join(", ") + ")";
  1330.             this._dbConnection.executeSimpleSQL(statement);
  1331.         }
  1332.     },
  1333.  
  1334.  
  1335.     _dbMigrate : function (oldVersion) {
  1336.         this.log("Attempting to migrate from version " + oldVersion);
  1337.  
  1338.         if (oldVersion > DB_VERSION) {
  1339.             this.log("Downgrading to version " + DB_VERSION);
  1340.             // User's DB is newer. Sanity check that our expected columns are
  1341.             // present, and if so mark the lower version and merrily continue
  1342.             // on. If the columns are borked, something is wrong so blow away
  1343.             // the DB and start from scratch. [Future incompatible upgrades
  1344.             // should swtich to a different table or file.]
  1345.  
  1346.             if (!this._dbAreExpectedColumnsPresent())
  1347.                 throw Components.Exception("DB is missing expected columns",
  1348.                                            Components.results.NS_ERROR_FILE_CORRUPTED);
  1349.  
  1350.             // Change the stored version to the current version. If the user
  1351.             // runs the newer code again, it will see the lower version number
  1352.             // and re-upgrade (to fixup any entries the old code added).
  1353.             this._dbConnection.schemaVersion = DB_VERSION;
  1354.             return;
  1355.         }
  1356.  
  1357.         // Upgrade to newer version...
  1358.  
  1359.         this._dbConnection.beginTransaction();
  1360.  
  1361.         try {
  1362.             for (let v = oldVersion + 1; v <= DB_VERSION; v++) {
  1363.                 this.log("Upgrading to version " + v + "...");
  1364.                 let migrateFunction = "_dbMigrateToVersion" + v;
  1365.                 this[migrateFunction]();
  1366.             }
  1367.         } catch (e) {
  1368.             this.log("Migration failed: "  + e);
  1369.             this._dbConnection.rollbackTransaction();
  1370.             throw e;
  1371.         }
  1372.  
  1373.         this._dbConnection.schemaVersion = DB_VERSION;
  1374.         this._dbConnection.commitTransaction();
  1375.         this.log("DB migration completed.");
  1376.     },
  1377.  
  1378.  
  1379.     /*
  1380.      * _dbMigrateToVersion2
  1381.      *
  1382.      * Version 2 adds a GUID column. Existing logins are assigned a random GUID.
  1383.      */
  1384.     _dbMigrateToVersion2 : function () {
  1385.         // Check to see if GUID column already exists.
  1386.         let exists = true;
  1387.         try { 
  1388.             let stmt = this._dbConnection.createStatement(
  1389.                            "SELECT guid FROM moz_logins");
  1390.             // (no need to execute statement, if it compiled we're good)
  1391.             stmt.finalize();
  1392.         } catch (e) {
  1393.             exists = false;
  1394.         }
  1395.  
  1396.         // Add the new column and index only if needed.
  1397.         if (!exists) {
  1398.             this._dbConnection.executeSimpleSQL(
  1399.                 "ALTER TABLE moz_logins ADD COLUMN guid TEXT");
  1400.  
  1401.             this._dbConnection.executeSimpleSQL(
  1402.                 "CREATE INDEX IF NOT EXISTS " +
  1403.                     "moz_logins_guid_index ON moz_logins (guid)");
  1404.         }
  1405.  
  1406.         // Get a list of IDs for existing logins
  1407.         let ids = [];
  1408.         let query = "SELECT id FROM moz_logins WHERE guid isnull";
  1409.         let stmt;
  1410.         try {
  1411.             stmt = this._dbCreateStatement(query);
  1412.             while (stmt.step())
  1413.                 ids.push(stmt.row.id);
  1414.         } catch (e) {
  1415.             this.log("Failed getting IDs: " + e);
  1416.             throw e;
  1417.         } finally {
  1418.             stmt.reset();
  1419.         }
  1420.  
  1421.         // Generate a GUID for each login and update the DB.
  1422.         query = "UPDATE moz_logins SET guid = :guid WHERE id = :id";
  1423.         for each (let id in ids) {
  1424.             let params = {
  1425.                 id:   id,
  1426.                 guid: this._uuidService.generateUUID().toString()
  1427.             };
  1428.  
  1429.             try {
  1430.                 stmt = this._dbCreateStatement(query, params);
  1431.                 stmt.execute();
  1432.             } catch (e) {
  1433.                 this.log("Failed setting GUID: " + e);
  1434.                 throw e;
  1435.             } finally {
  1436.                 stmt.reset();
  1437.             }
  1438.         }
  1439.     },
  1440.  
  1441.  
  1442.     /*
  1443.      * _dbMigrateToVersion3
  1444.      *
  1445.      * Version 3 adds a encType column.
  1446.      */
  1447.     _dbMigrateToVersion3 : function () {
  1448.         // Check to see if encType column already exists.
  1449.         let exists = true;
  1450.         let query = "SELECT encType FROM moz_logins";
  1451.         let stmt;
  1452.         try { 
  1453.             stmt = this._dbConnection.createStatement(query);
  1454.             // (no need to execute statement, if it compiled we're good)
  1455.             stmt.finalize();
  1456.         } catch (e) {
  1457.             exists = false;
  1458.         }
  1459.  
  1460.         // Add the new column and index only if needed.
  1461.         if (!exists) {
  1462.             query = "ALTER TABLE moz_logins ADD COLUMN encType INTEGER";
  1463.             this._dbConnection.executeSimpleSQL(query);
  1464.  
  1465.             query = "CREATE INDEX IF NOT EXISTS " +
  1466.                         "moz_logins_encType_index ON moz_logins (encType)";
  1467.             this._dbConnection.executeSimpleSQL(query);
  1468.         }
  1469.  
  1470.         // Get a list of existing logins
  1471.         let logins = [];
  1472.         query = "SELECT id, encryptedUsername, encryptedPassword " +
  1473.                     "FROM moz_logins WHERE encType isnull";
  1474.         try {
  1475.             stmt = this._dbCreateStatement(query);
  1476.             while (stmt.step()) {
  1477.                 let params = { id: stmt.row.id };
  1478.                 if (stmt.row.encryptedUsername.charAt(0) == '~' ||
  1479.                     stmt.row.encryptedPassword.charAt(0) == '~')
  1480.                     params.encType = ENCTYPE_BASE64;
  1481.                 else
  1482.                     params.encType = ENCTYPE_SDR;
  1483.                 logins.push(params);
  1484.             }
  1485.         } catch (e) {
  1486.             this.log("Failed getting logins: " + e);
  1487.             throw e;
  1488.         } finally {
  1489.             stmt.reset();
  1490.         }
  1491.  
  1492.         // Determine encryption type for each login and update the DB.
  1493.         query = "UPDATE moz_logins SET encType = :encType WHERE id = :id";
  1494.         for each (params in logins) {
  1495.             try {
  1496.                 stmt = this._dbCreateStatement(query, params);
  1497.                 stmt.execute();
  1498.             } catch (e) {
  1499.                 this.log("Failed setting encType: " + e);
  1500.                 throw e;
  1501.             } finally {
  1502.                 stmt.reset();
  1503.             }
  1504.         }
  1505.         
  1506.     },
  1507.  
  1508.  
  1509.     /*
  1510.      * _dbAreExpectedColumnsPresent
  1511.      *
  1512.      * Sanity check to ensure that the columns this version of the code expects
  1513.      * are present in the DB we're using.
  1514.      */
  1515.     _dbAreExpectedColumnsPresent : function () {
  1516.         let query = "SELECT " +
  1517.                        "id, " +
  1518.                        "hostname, " +
  1519.                        "httpRealm, " +
  1520.                        "formSubmitURL, " +
  1521.                        "usernameField, " +
  1522.                        "passwordField, " +
  1523.                        "encryptedUsername, " +
  1524.                        "encryptedPassword, " +
  1525.                        "guid, " +
  1526.                        "encType " +
  1527.                     "FROM moz_logins";
  1528.         try { 
  1529.             let stmt = this._dbConnection.createStatement(query);
  1530.             // (no need to execute statement, if it compiled we're good)
  1531.             stmt.finalize();
  1532.         } catch (e) {
  1533.             return false;
  1534.         }
  1535.  
  1536.         query = "SELECT " +
  1537.                    "id, " +
  1538.                    "hostname " +
  1539.                 "FROM moz_disabledHosts";
  1540.         try { 
  1541.             let stmt = this._dbConnection.createStatement(query);
  1542.             // (no need to execute statement, if it compiled we're good)
  1543.             stmt.finalize();
  1544.         } catch (e) {
  1545.             return false;
  1546.         }
  1547.  
  1548.         this.log("verified that expected columns are present in DB.");
  1549.         return true;
  1550.     },
  1551.  
  1552.  
  1553.     /*
  1554.      * _dbCleanup
  1555.      *
  1556.      * Called when database creation fails. Finalizes database statements,
  1557.      * closes the database connection, deletes the database file.
  1558.      */
  1559.     _dbCleanup : function (backup) {
  1560.         this.log("Cleaning up DB file - close & remove & backup=" + backup)
  1561.  
  1562.         // Create backup file
  1563.         if (backup) {
  1564.             let backupFile = this._signonsFile.leafName + ".corrupt";
  1565.             this._storageService.backupDatabaseFile(this._signonsFile, backupFile);
  1566.         }
  1567.  
  1568.         // Finalize all statements to free memory, avoid errors later
  1569.         for (let i = 0; i < this._dbStmts.length; i++)
  1570.             this._dbStmts[i].statement.finalize();
  1571.         this._dbStmts = [];
  1572.  
  1573.         // Close the connection, ignore 'already closed' error
  1574.         try { this._dbConnection.close() } catch(e) {}
  1575.         this._signonsFile.remove(false);
  1576.     }
  1577.  
  1578. }; // end of nsLoginManagerStorage_mozStorage implementation
  1579.  
  1580. let component = [LoginManagerStorage_mozStorage];
  1581. function NSGetModule(compMgr, fileSpec) {
  1582.     return XPCOMUtils.generateModule(component);
  1583. }
  1584.